<?php
session_start();
/*
 * RedisAdminLite 2.0
 * 开源作品-PHP写的Redis管理工具(单文件绿色版)-RedisAdminLite 2.0
 * Redis在线管理工具
 * 程序特性：
 *      1. 单文件绿色版,部署超简单
 *      2. 支持Redis大部分操作(持续完善中...)
 *      3. 根据Redis大数据量特性进行操作优化
 *      4. 支持用户登录验证、IP验证等多种权限验证功能
 *      5. 支持多个数据库服务器切换
 * 使用方法：
 *      1. 复制本文件到web任意目录
 *      2. 修改配置信息为实际内容
 *      3. 在浏览器输入链接路径,开始使用
 * 说明：写这个程序主要是为了辅助项目中用到的Redis开发.因为项目开发任务繁重,所以这个小程序暂时只能提供浏览、查询、添加、删除等功能(足以应付项目开发需要). 现在,要把精力转移到主项目开发中, 等以后有精力了再继续完善更多功能.如果您有兴趣一起完善这个小程序,欢迎使用git一起开发~感谢！
 * 项目周期：
 *      2015-8-13 立项开发
 *      2015-8-14 框架基本完成
 * 版权：不限商业用途! 如有修改代码请保留原著署名,并把修改后的代码回馈到作者邮箱：<14507247@qq.com>,促进本产品进步.做一个有公德心的正能量程序员~
 *      作者博客：http://www.cnblogs.com/sochishun/ 欢迎交流~
 * @since 1.0 <2015-8-13> SoChishun <14507247@qq.com> Added.
 * @since 1.1 <2015-10-8> SoChishun 新增键名列表对2.8.0以下版本的兼容,优化查询结果的显示效果.
 * @since 1.2 <2017-8-21> SoChishun 新增服务器密码验证功能和对PHP5.4以上版本的适配
 * @since 1.3 <2017-8-23> SoChishun html输出时新增htmlspecialchars过滤
 * @since 2.0 <2017-8-24> SoChishun 项目由SuRedisAdmin_PHP_1_0重构为RedisAdminLite，增加访问权限验证功能
 * @since 2.1 <2017-9-6> SoChishun 新增key_prefix选项
 * @see https://github.com/phpredis/phpredis
 */

// 版本号
define('VERSION', '2.0.0');
// 程序名称
define('APPNAME', 'RedisAdminLite');
// 缓存前缀
define('SESSION_PREFIX', 'radmlite');
$sess_id = SESSION_PREFIX . 'db';

/**
 * 页面访问验证配置
 */
$authconfig = [
    'admins' => ['admin' => 'admin123', 'testuser' => 'testpassword'], // 允许登录的帐号列表['登录名'=>'密码']
    'ips' => ['127.0.0.1', '120.42.7.179','120.42.5.80'], // 允许访问的ip地址列表
    'current_authtype' => 3, // 指定页面访问验证类型：0:无需验证;1:帐号密码验证;2:IP验证;3:混合验证(先IP验证,IP验证失败则转入帐号密码验证
];
// 全局配置
$config = array(
    'page_size' => 1000, // 侧边栏键名列表分页数量
    'default_db' => 'db0', // 默认数据库
    'server' => array(// 多服务器设置
        /* 本地服务器 */
        'localhost' => array(
            'host' => '127.0.0.1',
            'port' => 6379,
            'key_prefix' => [],
        ),
        /* 远程服务器 */
        'remote' => array(
            'host' => 'r-2ze886a71f256e74.redis.rds.aliyuncs.com',
            'port' => 6379,
            'password' => '1Os7jXnnrRPaVQ93',
            'key_prefix' => ['fanwe0000001:', 'fanwe0000002:'],
        ),
        /* rinetd端口映射 */
        'rinetd' => array(
            'host' => '47.93.42.85',
            'port' => 6379,
            'password' => '1Os7jXnnrRPaVQ93',
            'key_prefix' => ['fanwe0000001:', 'fanwe0000002:'],
        ),
    ),
    'current_server' => 'remote', // 指定当前活动的Redis服务器
);

// 访问权限验证
$current_authtype = $authconfig['current_authtype'];
$authstatus = true; // 权限验证状态
if ($current_authtype == 2 || $current_authtype == 3) { // IP验证
    $ip = $_SERVER['REMOTE_ADDR'];
    if (!in_array($ip, $authconfig['ips'])) {
        $authstatus = false; // IP验证失败
        if ($current_authtype == 2) {
            exit('IP [' . $ip . '] 禁止访问!');
        }
    }
}
if ($current_authtype == 1 || ($current_authtype == 3 && !$authstatus)) { // 帐号密码验证
    $loginid = isset($_SESSION[SESSION_PREFIX . 'user']) ? $_SESSION[SESSION_PREFIX . 'user'] : false;
    $token = get_token();
    if (!$token) {
        $token = generate_token();
    }
// 注销登录
    if (input('action') == 'logout') {
        logout();
        exit;
    }
    if (!$loginid) {
        checklogin($token, $authconfig['admins']);
        exit;
    }
}

// 连接redis
$redis = new Redis();
if (is_null($redis)) {
    die('Redis Create Failure!');
}

try {
    $server = $config['server'][$config['current_server']];
    $key_prefix = isset($server['key_prefix']) ? $server['key_prefix'] : '';
    $redis->connect($server['host'], $server['port']);
    if (!empty($server['password'])) {
        $redis->auth($server['password']);
    }
} catch (Exception $ex) {
    echo 'Redis Connect Failure:', $ex->getMessage();
    exit;
}

// 全局变量
$q = input('q');
$dbname = input('db'); // 当前数据库名称
$key = input('key'); // 键名
$action = input('action'); // 操作
$pagesize = input('pagesize', $config['page_size']);
$pageid = input('pageid', 1);
$message = '';
// 表单操作 2015-8-13 SoChishun Added.
if ($action) {
    $msg = false;
    switch ($action) {
        case 'delete':
            $msg = SuRedisAdmin::action_delete($redis, $key);
            break;
        case 'add':
            $msg = SuRedisAdmin::action_add($redis);
            break;
        case 'copy':
            $msg = SuRedisAdmin::action_copy($redis);
            break;
        case 'modify':
            $msg = SuRedisAdmin::action_modify($redis);
            break;
        case 'search':
            $msg = SuRedisAdmin::action_search($redis);
            break;
        case 'cleardb':
            if (isset($_SESSION[$sess_id])) {
                $dbname = $_SESSION[$sess_id];
                $db_id = substr($dbname, 2);
                $redis->select($db_id);
                $redis->flushDB();
                unset($_SESSION[$sess_id]);
                $msg = ajaxMsg(true, '清空数据库[' . $dbname . ']成功!');
            } else {
                $msg = ajaxMsg(false, '数据库不存在!');
            }
            break;
    }
    if ($msg) {
        $msg_info = $msg['info'];
        if (is_array($msg_info)) {
            $msg_info = implode('', $msg_info);
        }
        $message = $msg['status'] ? '<strong class="green">' . $msg_info . '</strong>' : '<strong class="red">' . $msg_info . '</strong>';
    }
}
?>
<!DOCTYPE html>
<html>
    <head>
        <meta charset="UTF-8">
        <title><?php echo ($key ? short_text($key, 16, true) . ' - ' : ''), APPNAME, ' - ', VERSION ?></title>
        <style type="text/css">
            body, td, th{font-size:13px; color:#333;}
            aside {width:180px; float:left;}
            aside fieldset div { max-height: 600px; overflow: auto;}
            aside li a:last-child {display:inline-block; width:100px;overflow: hidden;white-space: nowrap; text-overflow: ellipsis;}
            a{text-decoration: none;}
            ul{margin:0;}
            li{list-style: none; margin-left: -40px;}
            li.active { font-weight: bold; color:#F00;}
            li.active a{ color:#F00;}
            main { max-width: 960px; float:left;}
            footer { clear: left; padding:10px; text-align: right; color:#666; border-top:solid 2px #CCC;}
            a {text-decoration: none;}
            main h3 { margin-bottom: 5px;}
            h3 a { font-weight: normal; font-size:12px;}
            .red { color:#F00;}
            .green { color:#090;}
            aside h3 { margin:5px 0px;}
            .table_grid { max-height: 640px; overflow-y:auto;}
            .table_grid table {border: solid 1px #CCC; border-top: none; border-left: none; margin-bottom:10px;}
            .table_grid th, .table_grid td { border:solid 1px #CCC; border-right: none; border-bottom: none; padding:5px;word-break:break-all}
            .search_result{max-height:540px; overflow-y:auto; background-color:#efefef;}
            .grid { border:solid 1px #CCC; border-bottom:none; border-right: none;}
            .grid tr:hover{background-color: #efefef;}
            .grid th, .grid td { border:solid 1px #CCC; border-left:none; border-top:none;}
            .propertyinfo td { width:360px; word-break: break-all;}
        </style>
    </head>
    <body>
        <?php
        echo '<h1>', APPNAME, ' <a href="?new-window" title="新开一个页面" target="_blank" style="font-size:12px;">[N]</a></h1>';
        // 用户登录信息
        if (isset($loginid)) {
            echo $loginid;
            echo ' [<a href="?action=logout" onclick="return confirm(\'您确定要注销登录吗?\');">注销登录</a>]';
        }
        ?>
        <fieldset style="max-width:1115px;">
            <legend>Redis状态信息(<strong><?php echo $config['current_server'] ?></strong>) <a href="#">[+]</a></legend>
            <?php
            $dbs = [];
            $info = $redis->info(); // 获取Redis状态信息 2015-8-13 SoChishun Added.
            echo '<div style="max-height:250px; overflow:auto;display:none;">';
            echo '<table cellpadding="0" cellspacing="0" class="grid propertyinfo"><tr>';
            $i = 0;
            foreach ($info as $skey => $svalue) {
                if ($i > 0 && $i % 3 < 1) {
                    echo '</tr><tr>';
                }
                // 解析数据库id
                if (0 === strpos($skey, 'db')) {
                    $avals = explode(',', $svalue);
                    foreach ($avals as $sval) {
                        $atmp = explode('=', $sval);
                        $dbs[$skey][$atmp[0]] = $atmp[1];
                    }
                }
                // 解析版本号
                if ('redis_version' == $skey) {
                    $redis_version = $svalue;
                }
                echo '<th>', htmlspecialchars($skey), '</th><td>', htmlspecialchars($svalue), '</td>';
                $i++;
            }
            echo '</tr></table>';
            echo '</div>';
            ?>
        </fieldset>
        <aside>
            <?php
// 选择数据库 2015-8-14 SoChishun Added.
            if (!$dbname) {
                $dbname = isset($_SESSION[$sess_id]) ? $_SESSION[$sess_id] : $config['default_db'];
            }
            $dbconfig = [];
            if (array_key_exists($dbname, $dbs)) {
                $_SESSION[$sess_id] = $dbname;
                $dbconfig = $dbs[$dbname]; // [全局变量] 当前数据库信息 array('db'=>'','db_id'=>0,'keys'=>0,'expires'=>0,'avg_ttl'=>0);
                $dbconfig['db'] = $dbname;
                $dbconfig['db_id'] = substr($dbname, 2);
                $redis->select($dbconfig['db_id']);
            } else {
                echo '数据库 [' . $dbname . '] 不存在!';
            }

            echo '数据库：<select name="db">';
            foreach ($dbs as $skey => $svalue) {
                echo '<option value="', $skey, '"', ($dbname == $skey ? ' selected="selected"' : ''), '>', short_text($skey, 12), '</option>';
            }
            echo '</select>';
            ?>
            <div>
                <a href="?q=view">[新增]</a>
                <a href="?action=cleardb" onclick="return confirm('您确定要清空数据库中[<?php echo $dbname ?>]所有数据吗?此操作不可复原!');">[清空]</a>
                <a href="?q=search">[搜索]</a>
            </div>
            <fieldset><legend>键名：</legend>
                <div>
                    <ul>
                        <?php
                        // 获取当前数据库所有键值
                        if ($dbconfig) { // 如果redis数据库不是空的则显示键名
                            $pagecount = ceil($dbconfig['keys'] / $pagesize);
                            SuRedisAdmin::view_aside_keys($redis, $pageid, $pagesize, $redis_version, $key, $key_prefix);
                        }
                        ?>
                    </ul>
                </div>
                <div class="pager" style="max-width: 180px; word-break: break-all; margin-top:5px;" title="手动修改浏览器地址栏参数pagesize可以改变分页尺寸">
                    <?php
                    if ($dbconfig) {
                        // 分页功能 2015-8-14 SoChishun Added.
                        echo '共', $dbconfig['keys'], '行,每页', $pagesize, '行, 页次', $pageid, '/', $pagecount, '<br />页码：';
                        for ($i = 1; $i <= $pagecount; $i++) {
                            echo '<a href="?pagesize=', $pagesize, '&pageid=', $i, '" title="转到第', $i, '页">', $i, '</a> ';
                        }
                    }
                    ?>
                </div>
        </aside>
        <main>
            <?php
            switch ($q) {
                case 'view':
                    if ($key) {
                        SuRedisAdmin::view_edit_form($redis, $key);
                    } else {
                        SuRedisAdmin::view_add_form();
                    }
                    break;
                case 'copy':
                    if ($key) {
                        SuRedisAdmin::view_copy_form($redis, $key);
                    } else {
                        echo '<strong class="red">拷贝失败：键名无效</strong>';
                    }
                    break;
                case 'search':
                    SuRedisAdmin::view_search_form();
                    break;
            }
            echo $message;
            ?>
        </main>
        <div style="clear: both"></div>
        <footer>
            Copyright (c) 2017 RedisAdminLite;
        </footer>
        <script src="//code.jquery.com/jquery-1.11.3.min.js"></script>
        <script>
                    $('legend a').click(function () {
                        var 　$a = $(this);
                        if ($a.text() == '[-]') {
                            $a.text('[+]').attr('title', '展开');
                            $a.parent().next().hide();
                        } else {
                            $a.text('[-]').attr('title', '收起');
                            $a.parent().next().show();
                        }
                    })
                    $('select[name="db"]').change(function () {
                        var db = $(this).val();
                        if (db.length > 0) {
                            location.href = '?db=' + db;
                        }
                    })
        </script>
    </body>
</html>
<?php

/**
 * 业务类
 * @since 1.0 <2015-8-13> SoChishun Added.
 */
class SuRedisAdmin {

    /**
     * 显示边栏键值列表视图
     * @param Redis $redis Redis实例对象
     * @param integer $pageid 分页id
     * @param integer $pagesize 分页尺寸
     * @param string $version redis版本号
     * @param string $curkey 当前输入的键名
     * @since 1.0 <2015-8-14> SoChishun Added.
     * @since 2.0 <2015-10-8> SoChishun 新增对2.8.0之前版本的兼容方法
     * @since 2.1 <2017-8-25> SoChishun $pageconf参数重构为$pageid,$pagesize，增加$curkey参数
     * @since 2.2 <2017-9-6> SoChishun 新增key_prefix选项
     */
    static function view_aside_keys($redis, $pageid, $pagesize, $version, $curkey, $key_prefix) {
        if (!is_numeric($pageid) || !is_numeric($pagesize)) {
            die('分页参数无效');
        }
        $row_start = ($pageid - 1) * $pagesize;
        $row_end = $pageid * $pagesize;
        if (version_compare('2.8.0', $version) < 0) {
            self::keys_keys($redis, $row_start, $row_end, $curkey, $key_prefix);
        } else {
            self::keys_scan($redis, $row_start, $row_end, $curkey, $key_prefix);
        }
    }

    /**
     * keys方法获取键名
     * <br />2.8.0以下版本适用
     * @param Redis $redis
     * @param integer $row_start
     * @param integer $row_end
     * @param string $curkey 当前输入的键名
     * @since 1.0 <2015-10-8> SoChishun Added.
     */
    static function keys_keys($redis, $row_start, $row_end, $curkey, $key_prefix) {
        //ob_end_clean(); 服务器启用了PHP缓存，这里要注释，否则会版面会错乱
        $akeys = $redis->keys('*');
        if ($akeys) {
            $i = 1;
            $title = '';
            foreach ($akeys as $skey) {
                if ($i > $row_end) {
                    break;
                }
                if ($i >= $row_start) {
                    $len = 20 - strlen($i);
                    if ($key_prefix) {
                        foreach ($key_prefix as $prefix) {
                            if (0 === strpos($skey, $prefix)) {
                                $key_prefix_len = strlen($prefix);
                                $title = ($key_prefix_len > 0) ? substr($skey, $key_prefix_len) : $skey;
                                break;
                            }
                        }
                    }
                    $title = (strlen($title) > $len ? substr($title, 0, $len) . '..' : $title);
                    echo '<li', ($curkey == $skey ? ' class="active"' : ''), '>', $i, '.<a href="?q=view&key=', urlencode($skey), '" title="', $skey, '">', $title, '</a></li>';
                    //flush(); 服务器启用了PHP缓存，这里要注释，否则会版面会错乱
                }
                $i++;
            }
        }
    }

    /**
     * scan方法获取键名
     * <br />2.8.0以上版本适用
     * @param Redis $redis
     * @param integer $row_start
     * @param integer $row_end
     * @param string $curkey 当前输入的键名
     * @since 1.0 <2015-10-8> SoChishun Added.
     */
    static function keys_scan($redis, $row_start, $row_end, $curkey, $key_prefix) {
        $i = 0;
        $title = '';
        $is_break = false;
        $it = NULL; /* Initialize our iterator to NULL */
        $redis->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY); /* retry when we get no keys back */
        while ($akeys = $redis->scan($it)) {
            if ($is_break) {
                break;
            }
            foreach ($akeys as $skey) {
                if ($i > $row_end) {
                    $is_break = TRUE;
                    break;
                }
                if ($i >= $row_start) {
                    $len = 20 - strlen($i);
                    if ($key_prefix) {
                        foreach ($key_prefix as $prefix) {
                            if (0 === strpos($skey, $prefix)) {
                                $key_prefix_len = strlen($prefix);
                                $title = ($key_prefix_len > 0) ? substr($skey, $key_prefix_len) : $skey;
                                break;
                            }
                        }
                    }
                    $title = (strlen($title) > $len ? substr($title, 0, $len) . '..' : $title);
                    echo '<li', ($curkey == $skey ? ' class="active"' : ''), '>', $i, '.<a href="?q=view&key=', urlencode($skey), '" title="', $skey, '">', $title, '</a></li>';
                }
                $i++;
            }
            // echo "No more keys to scan!\n";
        }
    }

    /**
     * 显示编辑表单
     * @param Redis $redis Redis实例对象
     * @param string $key 键名
     * @param boolean $is_copy 是否拷贝
     * @since 1.0 <2015-8-13> SoChishun Added.
     */
    static function view_edit_form($redis, $key) {
        echo '<h3>编辑：', $key, ' <a href="?action=delete&key=', urlencode($key), '">[删除]</a><a href="?q=copy&key=', urlencode($key), '">[拷贝]</a></h3>';
        if ($redis->exists($key)) {
            echo '<form method="post">';
            $val = false;
            $type = $redis->type($key); // int
            switch ($type) {
                case Redis::REDIS_HASH:
                    $val = $redis->hGetAll($key);
                    echo '<div>类型：REDIS_HASH</div>';
                    echo '<div class="table_grid"><table cellspacing="0">';
                    foreach ($val as $th => $td) {
                        echo '<tr><th>', $th, '</th><td><input type="text" name="', $th, '" value="', htmlspecialchars($td), '" /></td></tr>';
                    }
                    echo '<tr><td colspan="2">', htmlspecialchars(json_encode($val)), '</td></tr>';
                    echo '</table></div>';
                    break;
                case Redis::REDIS_LIST:
                    echo '<div>类型：REDIS_LIST</div>';
                    $val = $redis->lRange($key, 0, -1);
                    echo '<div class="table_grid"><table cellspacing="0">';
                    foreach ($val as $td) {
                        echo '<tr><td>', htmlspecialchars($td), '</td></tr>';
                    }
                    echo '<tr><td>', htmlspecialchars(json_encode($val)), '</td></tr>';
                    echo '</table></div>';
                    break;
                case Redis::REDIS_NOT_FOUND:
                    echo '<div>类型：REDIS_NOT_FOUND</div>';
                    break;
                case Redis::REDIS_SET:
                    echo '<div>类型：REDIS_SET</div>';
                    $val = $redis->sMembers($key);
                    echo '<div class="table_grid"><table cellspacing="0">';
                    echo '<tr><th>值</th></tr>';
                    foreach ($val as $td) {
                        echo '<tr><td>', htmlspecialchars($td), '</td></tr>';
                    }
                    echo '<tr><td>', htmlspecialchars(json_encode($val)), '</td></tr>';
                    echo '</table></div>';
                    break;
                case Redis::REDIS_STRING:
                    echo '<div>类型：REDIS_STRING</div>';
                    $val = $redis->get($key);
                    echo '<div class="table_grid"><table cellspacing="0">';
                    echo '<tr><td><input type="text" name="value" value="', htmlspecialchars($val), '" /></td></tr>';
                    echo '</table></div>';
                    break;
                case Redis::REDIS_ZSET:
                    echo '<div>类型：REDIS_ZSET</div>';
                    echo '<div class="table_grid"><table cellspacing="0">';
                    echo '<tr><th>值</th><th>排序</th></tr>';
                    $val = array();
                    $it = NULL;
                    $redis->setOption(Redis::OPT_SCAN, Redis::SCAN_RETRY);
                    while ($arr_matches = $redis->zscan($key, $it, '*')) {
                        foreach ($arr_matches as $str_mem => $f_score) {
                            echo '<tr><th>', $str_mem, '</td><td>', htmlspecialchars($f_score), '</td></tr>';
                            $val[$str_mem] = $f_score;
                        }
                    }
                    echo '<tr><td colspan="2">', htmlspecialchars(json_encode($val)), '</td></tr>';
                    echo '</table></div>';
                    break;
            }
            if ($val) {
                echo '<input type="hidden" name="key" value="', $key, '" />';
                echo '<input type="hidden" name="type" value="', $type, '" />';
                echo '<input type="hidden" name="action" value="modify" />';
                echo '<button type="submit">保存</button><button type="reset">重置</button>';
            }
            echo '</form>';
        } else {
            echo '<div>键名不存在!</div>';
        }
    }

    /**
     * 显示复制表单
     * @param Redis $redis Redis实例对象
     * @param string $key 键名
     * @since 1.0 <2015-8-13> SoChishun Added.
     */
    static function view_copy_form($redis, $key) {
        $type = $redis->type($key); // int
        echo '<h3>拷贝：', $key, '</h3>';
        if ($redis->exists($key)) {
            echo '<form method="post">';
            $val = false;
            switch ($type) {
                case Redis::REDIS_HASH:
                    $val = $redis->hGetAll($key);
                    echo '<div class="table_grid"><table cellspacing="0">';
                    echo '<tr><th>类型：</th><td><input type="text" name="type" value="HASH" readonly="readonly" /></td></tr>';
                    echo '<tr><th>键名：</th><td><input type="text" name="key" value="', $key, '" /><input type="hidden" name="key_old" value="', $key, '" /></td></tr>';
                    echo '<tr><th>键值：</th><td>';
                    echo '<p style="color:#F00;">HASE类型格式：{"id":1,"name":"this is \"my Name\""}<br />LIST类型格式：["name","this is \"my Name\""]<br /></p>';
                    echo '<textarea name="value" cols="90" rows="9">', json_encode($val), '</textarea></td></tr>';
                    echo '</table></div>';
                    break;
                case Redis::REDIS_LIST:
                    $val = $redis->lGet($key);
                    break;
                case Redis::REDIS_NOT_FOUND:
                    break;
                case Redis::REDIS_SET:
                    $val = $redis->sGetMembers($key);
                    break;
                case Redis::REDIS_STRING:
                    $val = $redis->get($key);
                    break;
                case Redis::REDIS_ZSET:
                    $val = $redis->get($key);
                    break;
            }
            if ($val) {
                echo '<input type="hidden" name="action" value="copy" />';
                echo '<button type="submit">保存</button><button type="reset">重置</button>';
            }
            echo '</form>';
        } else {
            echo '<div>键名不存在!</div>';
        }
    }

    /**
     * 显示新增表单
     * @since 1.0 <2015-8-13> SoChishun Added.
     */
    static function view_add_form() {
        echo '<h3>新增数据</h3>';
        echo '<form method="post">';
        echo '<div class="table_grid"><table cellspacing="0">';
        echo '<tr><th>类型：</th><td>';
        $atypes = array('HASH', 'LIST', 'SET', 'STRING', 'ZSET');
        echo '<select name="type">';
        foreach ($atypes as $stype) {
            echo '<option value="', $stype, '">', $stype, '</option>';
        }
        echo '</select>';
        echo '</td></tr>';
        echo '<tr><th>键名：</th><td><input type="text" name="key" /></td></tr>';
        echo '<tr><th>键值：</th><td>';
        echo '<p style="color:#F00;">HASE类型格式：{"id":1,"name":"this is \"my Name\""}<br />LIST类型格式：["name","this is \"my Name\""]<br /></p>';
        echo '<textarea name="value" cols="90" rows="9"></textarea></td></tr>';
        echo '</table></div>';
        echo '<button type="submit">保存</button><button type="reset">重置</button>';
        echo '<input type="hidden" name="action" value="add" />';
        echo '</form>';
    }

    /**
     * 显示查询表单
     * @since 1.0 <2015-8-14> SoChishun Added.
     */
    static function view_search_form() {
        echo '<h3>搜索</h3>';
        echo '<form method="post">';
        echo '<div class="table_grid"><table cellspacing="0">';
        echo '<tr><th>键名：</th><td><p class="red">支持模糊搜索，如：*words*.</p><input type="text" name="key" value="', input('key'), '" /></td></tr>';
        // echo '<tr><th>键值：</th><td><p class="red">支持模糊搜索，如：*words*.</p><input type="text" name="value" /></td></tr>';
        echo '</table></div>';
        echo '<button type="submit">立即搜索</button>';
        echo '<input type="hidden" name="action" value="search" />';
        echo '</form>';
    }

    /**
     * 删除键操作
     * @param Redis $redis
     * @param string $key
     * @return string
     * @since 1.0 <2015-8-13> SoChishun Added.
     */
    static function action_delete($redis, $key) {
        if (!$key) {
            redirect(false, '删除失败: 键名不存在!');
        } else {
            $redis->delete($key);
            redirect(true, '键 [' . $key . '] 删除成功!', '?q=view');
        }
    }

    /**
     * 表单新增数据操作
     * @param Redis $redis
     * @return array
     * @since 1.0 <2015-8-14> SoChishun Added.
     */
    static function action_add($redis) {
        $data = $_POST;
        $errs = false;
        if ($data['key']) {
            if ($redis->exists($data['key'])) {
                $errs[] = '键值已存在!';
            }
        } else {
            $errs [] = '键名无效!';
        }
        if (strlen($data['value']) < 1) {
            $errs [] = '键值无效!';
        }
        if ($errs) {
            redirect(false, $errs);
        }
        $key = $data['key'];
        switch ($data['type']) {
            case 'HASH':
                $avalue = json_decode($data['value'], true);
                $redis->hMset($key, $avalue);
                break;
            case 'LIST':
                $avalue = json_decode($data['value'], true);
                foreach ($avalue as $svalue) {
                    $redis->lPush($key, $svalue);
                }
                break;
            case 'SET':
                $avalue = json_decode($data['value'], true);
                foreach ($avalue as $svalue) {
                    $redis->sAdd($key, $svalue);
                }
                break;
            case 'STRING':
                try {
                    $redis->set($key, $data['value']);
                } catch (Exception $ex) {
                    echo $ex->getMessage();
                }
                break;
            case 'ZSET':
                $avalue = json_decode($data['value'], true);
                $i = 0;
                foreach ($avalue as $svalue) {
                    $redis->zAdd($key, $i, $svalue);
                    $i++;
                }
                break;
        }
        header('location:?q=view&key=' . $key);
    }

    static function action_copy($redis) {
        $key = input('key');
        $key_old = input('key_old');
        if ($key == $key_old) {
            return ajaxMsg(false, '复制操作失败：新键名不能和旧键名一样!<br />如果想编辑高级数据，请按照以下步骤操作：<br />1. 删除原键.<br />2. 新增新键.');
        }
    }

    /**
     * 修改值
     * @param type $redis
     */
    static function action_modify($redis) {
        $data = $_POST;
        $errs = [];
        if (empty($data['key'])) {
            $errs [] = '键名无效!';
        }
        if (empty($data['value'])) {
            $errs [] = '键值无效!';
        }
        if ($errs) {
            redirect(false, $errs);
        }
        $key = $data['key'];
        switch (intval($data['type'])) {
            case Redis::REDIS_HASH:
                $avalue = json_decode($data['value'], true);
                $redis->hMset($key, $avalue);
                break;
            case Redis::REDIS_LIST:
                $avalue = json_decode($data['value'], true);
                foreach ($avalue as $svalue) {
                    $redis->lPush($key, $svalue);
                }
                break;
            case Redis::REDIS_SET:
                $avalue = json_decode($data['value'], true);
                foreach ($avalue as $svalue) {
                    $redis->sAdd($key, $svalue);
                }
                break;
            case Redis::REDIS_STRING:
                try {
                    $redis->set($key, $data['value']);
                } catch (Exception $ex) {
                    echo $ex->getMessage();
                }
                break;
            case Redis::REDIS_ZSET:
                $avalue = json_decode($data['value'], true);
                $i = 0;
                foreach ($avalue as $svalue) {
                    $redis->zAdd($key, $i, $svalue);
                    $i++;
                }
                break;
        }
        header('location:?q=view&key=' . $key);
    }

    /**
     * 搜索操作
     * @param Redis $redis
     * @return array
     * @since 1.0 <2015-8-14> SoChishun Added.
     */
    static function action_search($redis) {
        $data = $_POST;
        $errs = false;
        if (!$data['key']) {
            $errs [] = '键名无效!';
        }
        if ($errs) {
            return ajaxMsg(false, $errs);
        }
        $key = $data['key'];
        $akeys = $redis->keys($key);
        if ($akeys) {
            $i = 0;
            $n = count($akeys);
            $items = array();
            $items[] = '<div style="margin-top:5px;">查询结果[' . $n . ']：</div>';
            $items[] = '<div class="search_result">';
            foreach ($akeys as $skey) {
                $items[] = '<div>' . $i . '.<a href="?q=view&key=' . urlencode($skey) . '" title="' . $skey . '">' . $skey . '</a></div>';
                $i++;
            }
            $items[] = '</div>';
            return ajaxMsg(true, $items);
        } else {
            return ajaxMsg(false, '<div style="margin-top:5px;">查询结果：</div><br />没有符合条件的数据! [查询条件：' . $key . '] <a href="?q=search">[返回搜索]</a>');
        }
    }

}

/**
 * 倒计时跳转
 * @param boolean $status
 * @param string|array $msg
 * @param string $url
 * @param integer $time
 * @since 2017-8-24
 */
function redirect($status, $msg, $url = '', $time = 3) {
    if (!$url) {
        $url = 'history.back();';
    } else {
        $url = 'location.href="' . $url . '"';
    }
    echo '<span style="color:', ($status ? '#090' : '#F00'), '">', (is_array($msg) ? implode('<br />', $msg) : $msg), '</span>(<span id="cd">' . $time . '</span>)';
    echo '<script>function countdown(){var i=' . $time . ';var $cd=document.getElementById("cd");function fn_cd(){i--;$cd.innerText=i;if(i<1){clearInterval(itr);' . $url . '}}var itr=setInterval(fn_cd,1000)}countdown();</script>';
    exit;
}

/**
 * 格式化返回消息
 * @param boolean $status
 * @param string $info
 * @return array
 * @since 1.0 <2015-8-13> SoChishun Added.
 */
function ajaxMsg($status, $info) {
    return array('status' => $status, 'info' => $info);
}

/**
 * 获取用户输入
 * @param string $name
 * @param mixed $defv
 * @return mixed
 * @since 1.0 <2015-8-13> SoChishun Added.
 */
function input($name, $defv = '') {
    return isset($_REQUEST[$name]) ? $_REQUEST[$name] : $defv;
}

/**
 * 截短字符串
 * @param string $str
 * @param integer $length
 * @param boolean $behind
 * @return string
 */
function short_text($str, $length, $behind = false) {
    $len = strlen($str);
    if ($len <= $length) {
        return $str;
    }
    return $behind ? '...' . substr($str, $len - $length) : substr($str, 0, $len) . '...';
}

/**
 * 获取表单令牌
 * @return string
 */
function get_token() {
    return isset($_SESSION[SESSION_PREFIX . 'token']) ? $_SESSION[SESSION_PREFIX . 'token'] : false;
}

/**
 * 生成表单令牌
 * @return string
 */
function generate_token() {
    $token = date('ymdhis') . rand(100, 999);
    $_SESSION[SESSION_PREFIX . 'token'] = $token;
    return $token;
}

/**
 * 检测用户登录
 */
function checklogin($token, $admins) {
    $posttoken = htmlspecialchars(input('token'));
    if ($posttoken) {
        generate_token(); // 重新生成token
        if ($posttoken != $token) {
            exit('非法来源!');
        }
        $loginid = strval(htmlspecialchars(input('user')));
        $loginpwd = htmlspecialchars(input('pass'));
        if (!$loginid || !$loginpwd) {
            exit('表单填写不完整!');
        }
        if (!array_key_exists($loginid, $admins)) {
            exit('帐号不存在!');
        }
        if ($loginpwd != $admins[$loginid]) {
            exit('密码错误!');
        }
        $_SESSION[SESSION_PREFIX . 'user'] = $loginid;
        header('location:?loginsuccess');
    } else {
        echo '<form method="post" action="?action=login">';
        echo '<input type="text" name="user" placeholder="用户名" required="required" />';
        echo '<input type="password" name="pass" placeholder="密码" required="required" />';
        echo '<input type="hidden" name="token" value="', $token, '" />';
        echo '<button type="submit">登录</button><button type="reset">重置</button> ';
        echo $_SERVER['REMOTE_ADDR'];
        echo '</form>';
    }
}

/**
 * 注销登录
 */
function logout() {
    unset($_SESSION[SESSION_PREFIX . 'user']);
    unset($_SESSION[SESSION_PREFIX . 'token']);
    header('location:?logoutsuccess');
}
